// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.dfa.lib.client;

import com.google.api.ads.common.lib.client.AdsSession;
import com.google.api.ads.common.lib.client.Endpoint;
import com.google.api.ads.common.lib.conf.ConfigurationHelper;
import com.google.api.ads.common.lib.exception.ValidationException;
import com.google.common.annotations.VisibleForTesting;

import org.apache.commons.configuration.Configuration;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * A {@code DfaSession} represents a single session of DFA use.
 *
 * @author Adam Rogal
 */
public class DfaSession implements AdsSession {

  /**
   * Enum representing the endpoint server, aka environment.
   * @author Kevin Winter
   */
  public static enum Environment implements Endpoint {
    PRODUCTION("https://advertisersapi.doubleclick.net"),
    TEST("https://advertisersapitest.doubleclick.net");

    private final String endpoint;

    private Environment(String endpoint) {
      this.endpoint = endpoint;
    }

    /**
     * @return the endpoint
     */
    public String getEndpoint() {
      return endpoint;
    }
  }

  private String username;
  private String password;
  private String authenticationToken;
  private String applicationName;

  private String usernameToImpersonate;
  private Long networkToImpersonate;
  private String impersonatedToken;
  private String endpoint;

  /**
   * Default constructor.
   */
  private DfaSession() {
    super();
  }

  /**
   * Sets the username to impersonate. Only superusers may impersonate.
   *
   * Only a username to impersonate or a network to impersonate should ever be
   * specified at the same time.
   */
  public void setUsernameToImpersonate(String usernameToImpersonate) {
    this.usernameToImpersonate = usernameToImpersonate;
  }

  /**
   * Gets the username to impersonate. Only superusers may impersonate.
   */
  public String getUsernameToImpersonate() {
    return usernameToImpersonate;
  }

  /**
   * Sets the network to impersonate. Only superusers may impersonate.
   *
   * Only a username to impersonate or a network to impersonate should ever be
   * specified at the same time.
   */
  public void setNetworkToImpersonate(Long networkToImpersonate) {
    this.networkToImpersonate = networkToImpersonate;
  }

  /**
   * Gets the network to impersonate. Only superusers may impersonate.
   */
  public Long getNetworkToImpersonate() {
    return networkToImpersonate;
  }

  /**
   * Sets the token necessary to impersonate. Only superusers may impersonate.
   */
  public void setImpersonatedToken(String impersonatedToken) {
    this.impersonatedToken = impersonatedToken;
  }

  /**
   * Gets the token necessary to impersonate. Only superusers may impersonate.
   */
  public String getImpersonatedToken() {
    return impersonatedToken;
  }

  /**
   * Indicates whether clients of this session will impersonate. A
   * username or network ID is required to impersonate; just an impersonated
   * token is not enough.
   */
  public boolean isImpersonating() {
    return (usernameToImpersonate != null || networkToImpersonate != null);
  }

  /**
   * Resets all settings related to impersonating in this session.
   *
   * You should call this method when switching between impersonating one
   * username/network and another to ensure that the old impersonation token
   * has been cleared.
   */
  public void stopImpersonating() {
    usernameToImpersonate = impersonatedToken = null;
    networkToImpersonate = null;
  }

  /**
   * Sets the token associated with this session.
   */
  public void setToken(String token) {
    this.authenticationToken = token;
  }

  /**
   * Gets the user name.
   */
  public String getUsername() {
    return username;
  }

  /**
   * Gets the password.
   */
  public String getPassword() {
    return password;
  }

  /**
   * Gets the token used for authentication.
   */
  public String getToken() {
    return authenticationToken;
  }

  /**
   * Gets the application name.
   */
  public String getApplicationName() {
    return applicationName;
  }

  /**
   * @return the endpoint
   */
  public String getEndpoint() {
    return endpoint;
  }

  /**
   * Determines if the session is for the supplied
   * {@link DfaSession.Environment}.
   */
  public boolean isEnvironment(Endpoint environment) {
    return environment.getEndpoint().equals(this.endpoint);
  }

  /**
   * Builder for DfaSession.
   */
  public static class Builder implements com.google.api.ads.common.lib.utils.Builder<DfaSession> {

    private DfaSession dfaSession;
    private String environmentString;
    @VisibleForTesting
    ConfigurationHelper configHelper;

    @VisibleForTesting
    Builder(ConfigurationHelper helper) {
      this.dfaSession = new DfaSession();
      this.configHelper = helper;
    }

    /**
     * Constructor.
     */
    public Builder() {
      this.dfaSession = new DfaSession();
      this.configHelper = new ConfigurationHelper();
    }

    /* (non-Javadoc)
     * @see com.google.api.ads.common.lib.utils.Builder#fromFile()
     */
    public Builder fromFile() {
      return fromFile(Builder.DEFAULT_CONFIGURATION_FILENAME);
    }

    /* (non-Javadoc)
     * @see com.google.api.ads.common.lib.utils.Builder#fromFile(java.lang.String)
     */
    public Builder fromFile(String path) {
      return from(configHelper.fromFile(path));
    }

    /* (non-Javadoc)
     * @see com.google.api.ads.common.lib.utils.Builder#fromFile(java.io.File)
     */
    public Builder fromFile(File path) {
      return from(configHelper.fromFile(path));
    }

    /* (non-Javadoc)
     * @see com.google.api.ads.common.lib.utils.Builder#fromFile(java.net.URL)
     */
    public Builder fromFile(URL path) {
      return from(configHelper.fromFile(path));
    }

    /**
     * Reads properties from the provided {@link Configuration} object.<br><br>
     * Known properties:
     * <ul>
     * <li>api.dfa.username</li>
     * <li>api.dfa.password</li>
     * <li>api.dfa.applicationName</li>
     * <li>api.dfa.token</li>
     * <li>api.dfa.environment, e.g. production or test</li>
     * </ul>
     *
     * @param config
     * @return Builder populated from the Configuration
     */
    public Builder from(Configuration config) {
      dfaSession.username = config.getString("api.dfa.username", null);
      dfaSession.password = config.getString("api.dfa.password", null);
      dfaSession.applicationName = config.getString("api.dfa.applicationName", null);
      dfaSession.authenticationToken = config.getString("api.dfa.token", null);
      dfaSession.endpoint = config.getString("api.dfa.endpoint", null);
      environmentString = config.getString("api.dfa.environment", null);

      return this;
    }

    /**
     * Includes the specified environment, e.g.
     * {@link Environment#TEST}
     */
    public Builder withEnvironment(Environment environment) {
      dfaSession.endpoint = environment.getEndpoint();
      return this;
    }

    /**
     * Override the endpoint server.
     */
    public Builder withEndpoint(String endpoint) {
      dfaSession.endpoint = endpoint;
      return this;
    }

    /**
     * Includes username and password.
     */
    public Builder withUsernameAndPassword(String username, String password) {
      dfaSession.username = username;
      dfaSession.password = password;
      return this;
    }

    /**
     * Includes a username with a hard-coded token to be used instead of
     * fetching a new one.
     */
    public Builder withUsernameAndToken(String username, String token) {
      dfaSession.username = username;
      dfaSession.authenticationToken = token;
      return this;
    }

    /**
     * Includes application name.
     */
    public Builder withApplicationName(String applicationName) {
      dfaSession.applicationName = applicationName;
      return this;
    }

    /**
     * Builds the {@code DfaSession}.
     * @return the built {@code DfaSession}
     * @throws ValidationException if the {@code DfaSession} did not validate
     */
    public DfaSession build() throws ValidationException {
      validate(dfaSession);
      return dfaSession;
    }

    /**
     * Validates the DFA session.
     */
    private void validate(DfaSession dfaSession) throws ValidationException {
      if (environmentString != null) {
        try {
          withEnvironment(Environment.valueOf(environmentString.toUpperCase()));
        } catch (IllegalArgumentException e) {
          throw new ValidationException(String.format("Environment [%s] not recognized.",
              environmentString), "api.dfa.environment", e);
        }
      }

      // Check for valid username.
      if (dfaSession.getUsername() == null) {
        throw new ValidationException("Username is required.", "username");
      }

      boolean usingPassword = dfaSession.getPassword() != null;
      boolean usingToken = dfaSession.getToken() != null;

      // Check that at least one auth mechanism is being use.
      if (!usingPassword && !usingToken) {
        throw new ValidationException(
            "Either username/password or username/token must be used.",
            "");
      }

      // Check that only one is being used.
      if (usingPassword && usingToken) {
        throw new ValidationException(
            "Both username/password and username/token cannot be used at the same time.",
            "");
      }

      // Make sure they specify an environment.
      try {
        new URL(dfaSession.getEndpoint());
      } catch (MalformedURLException e) {
        throw new ValidationException("Endpoint is required and must be a valid URL.",
            "endpoint", e);
      }
    }
  }
}
